nestjs教程

资源

  1. 文档地址

https://nestjs.bootcss.com/

  1. 让我们用Nestjs来重写一个CNode https://github.com/jiayisheji/blog/issues/19

  2. 视频教程 【Nestjs实战】Nodejs必学框架 | Nest企业级项目构建与开发实战

  3. Nest.js 从零到壹系列

知识点列表

安装

$ npm i -g @nestjs/cli
$ nest new project-name

使用cli创建模块

  1. 创建一个模块

    nest g resource xxx
    
  2. 创建一个service

    nest g service xxx
    
  3. 创建一个

全局模块

  1. 创建全局模块
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}
  1. 在需要的地方导入service就可以使用了

日志系统

  1. 安装模块

    npm i log4js stacktrace-js -S
    
  2. 创建配置文件

    // util/log4js.config.ts
    import * as path from 'path';
    const baseLogPath = path.resolve(__dirname, '../../logs'); // 日志要写入哪个目录
     
    const log4jsConfig = {
      appenders: {
        console: {
          type: 'console', // 会打印到控制台
        },
        access: {
          type: 'dateFile', // 会写入文件,并按照日期分类
          filename: `${baseLogPath}/access/access.log`, // 日志文件名,会命名为:access.20200320.log
          alwaysIncludePattern: true,
          pattern: 'yyyyMMdd',
          daysToKeep: 60,
          numBackups: 3,
          category: 'http',
          keepFileExt: true, // 是否保留文件后缀
        },
        app: {
          type: 'dateFile',
          filename: `${baseLogPath}/app-out/app.log`,
          alwaysIncludePattern: true,
          layout: {
            type: 'pattern',
            pattern: '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}',
          },
          // 日志文件按日期(天)切割
          pattern: 'yyyyMMdd',
          daysToKeep: 60,
          // maxLogSize: 10485760,
          numBackups: 3,
          keepFileExt: true,
        },
        errorFile: {
          type: 'dateFile',
          filename: `${baseLogPath}/errors/error.log`,
          alwaysIncludePattern: true,
          layout: {
            type: 'pattern',
            pattern: '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}',
          },
          // 日志文件按日期(天)切割
          pattern: 'yyyyMMdd',
          daysToKeep: 60,
          // maxLogSize: 10485760,
          numBackups: 3,
          keepFileExt: true,
        },
        errors: {
          type: 'logLevelFilter',
          level: 'ERROR',
          appender: 'errorFile',
        },
      },
      categories: {
        default: {
          appenders: ['console', 'app', 'errors'],
          level: 'DEBUG',
        },
        info: { appenders: ['console', 'app', 'errors'], level: 'info' },
        access: { appenders: ['console', 'app', 'errors'], level: 'info' },
        http: { appenders: ['access'], level: 'DEBUG' },
      },
      pm2: true, // 使用 pm2 来管理项目时,打开
      pm2InstanceVar: 'INSTANCE_ID', // 会根据 pm2 分配的 id 进行区分,以免各进程在写日志时造成冲突
    };
     
    export default log4jsConfig; 
    
  3. 创建实例

    import * as Path from 'path';
    import * as Log4js from 'log4js';
    import * as Util from 'util';
    import * as Moment from 'moment'; // 处理时间的工具
    import * as StackTrace from 'stacktrace-js';
    import Chalk from 'chalk';
    import config from './log4js.config';
     
    // 日志级别
    export enum LoggerLevel {
      ALL = 'ALL',
      MARK = 'MARK',
      TRACE = 'TRACE',
      DEBUG = 'DEBUG',
      INFO = 'INFO',
      WARN = 'WARN',
      ERROR = 'ERROR',
      FATAL = 'FATAL',
      OFF = 'OFF',
    }
     
    // 内容跟踪类
    export class ContextTrace {
      constructor(
        public readonly context: string,
        public readonly path?: string,
        public readonly lineNumber?: number,
        public readonly columnNumber?: number,
      ) {}
    }
     
    Log4js.addLayout('Awesome-nest', (logConfig: any) => {
      return (logEvent: Log4js.LoggingEvent): string => {
        let moduleName: string = '';
        let position: string = '';
     
        // 日志组装
        const messageList: string[] = [];
        logEvent.data.forEach((value: any) => {
          if (value instanceof ContextTrace) {
            moduleName = value.context;
            // 显示触发日志的坐标(行,列)
            if (value.lineNumber && value.columnNumber) {
              position = `${value.lineNumber}, ${value.columnNumber}`;
            }
            return;
          }
     
          if (typeof value !== 'string') {
            value = Util.inspect(value, false, 3, true);
          }
     
          messageList.push(value);
        });
     
        // 日志组成部分
        const messageOutput: string = messageList.join(' ');
        const positionOutput: string = position ? ` [${position}]` : '';
        const typeOutput: string = `[${logConfig.type}] ${logEvent.pid.toString()}   - `;
        const dateOutput: string = `${Moment(logEvent.startTime).format('YYYY-MM-DD HH:mm:ss')}`;
        const moduleOutput: string = moduleName ? `[${moduleName}] ` : '[LoggerService] ';
        let levelOutput: string = `[${logEvent.level}] ${messageOutput}`;
     
        // 根据日志级别,用不同颜色区分
        switch (logEvent.level.toString()) {
          case LoggerLevel.DEBUG:
            levelOutput = Chalk.green(levelOutput);
            break;
          case LoggerLevel.INFO:
            levelOutput = Chalk.cyan(levelOutput);
            break;
          case LoggerLevel.WARN:
            levelOutput = Chalk.yellow(levelOutput);
            break;
          case LoggerLevel.ERROR:
            levelOutput = Chalk.red(levelOutput);
            break;
          case LoggerLevel.FATAL:
            levelOutput = Chalk.hex('#DD4C35')(levelOutput);
            break;
          default:
            levelOutput = Chalk.grey(levelOutput);
            break;
        }
     
        return `${Chalk.green(typeOutput)}${dateOutput}  ${Chalk.yellow(moduleOutput)}${levelOutput}${positionOutput}`;
      };
    });
     
    // 注入配置
    Log4js.configure(config);
     
    // 实例化
    const logger = Log4js.getLogger();
    logger.level = LoggerLevel.TRACE;
     
    export class Logger {
      static trace(...args) {
        logger.trace(Logger.getStackTrace(), ...args);
      }
     
      static debug(...args) {
        logger.debug(Logger.getStackTrace(), ...args);
      }
     
      static log(...args) {
        logger.info(Logger.getStackTrace(), ...args);
      }
     
      static info(...args) {
        logger.info(Logger.getStackTrace(), ...args);
      }
     
      static warn(...args) {
        logger.warn(Logger.getStackTrace(), ...args);
      }
     
      static warning(...args) {
        logger.warn(Logger.getStackTrace(), ...args);
      }
     
      static error(...args) {
        logger.error(Logger.getStackTrace(), ...args);
      }
     
      static fatal(...args) {
        logger.fatal(Logger.getStackTrace(), ...args);
      }
     
      static access(...args) {
        const loggerCustom = Log4js.getLogger('http');
        loggerCustom.info(Logger.getStackTrace(), ...args);
      }
     
      // 日志追踪,可以追溯到哪个文件、第几行第几列
      static getStackTrace(deep: number = 2): string {
        const stackList: StackTrace.StackFrame[] = StackTrace.getSync();
        const stackInfo: StackTrace.StackFrame = stackList[deep];
     
        const lineNumber: number = stackInfo.lineNumber;
        const columnNumber: number = stackInfo.columnNumber;
        const fileName: string = stackInfo.fileName;
        const basename: string = Path.basename(fileName);
        return `${basename}(line: ${lineNumber}, column: ${columnNumber}): \n`;
      }
    } 
    
  4. 安装中间件

    nest g middleware logger middleware
    
  5. 配置中间件

    // logger.middleware.ts
    import { Injectable, NestMiddleware } from '@nestjs/common';
    import { Request, Response } from 'express';
    import { Logger } from '../common/util/log4js';
      
    // 函数式中间件
    export function logger(req: Request, res: Response, next: () => any) {
      const code = res.statusCode; // 响应状态码
      next();
      // 组装日志信息
      const logFormat = ` >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        Request original url: ${req.originalUrl}
        Method: ${req.method}
        IP: ${req.ip}
        Status code: ${code}
        Parmas: ${JSON.stringify(req.params)}
        Query: ${JSON.stringify(req.query)}
        Body: ${JSON.stringify(req.body)} \n  >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
      `;
      // 根据状态码,进行日志类型区分
      if (code >= 500) {
        Logger.error(logFormat);
      } else if (code >= 400) {
        Logger.warn(logFormat);
      } else {
        Logger.access(logFormat);
        Logger.log(logFormat);
      }
    } 
    
  6. 应用中间件

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { ValidationPipe } from '@nestjs/common';
    import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
    import { logger } from './middleware/logger.middleware';
    import * as express from 'express';
    
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      app.use(express.urlencoded({ extended: true }));
      app.use(logger);
      app.useGlobalPipes(new ValidationPipe());
      // 配置 Swagger
      const options = new DocumentBuilder()
        .setTitle('film-server')
        .setDescription('电影服务器接口')
        .setVersion('1.0')
        // .addTag('test') // 添加分组标签
        .build();
      const document = SwaggerModule.createDocument(app, options);
      SwaggerModule.setup('docs', app, document); 
     
      await app.listen(9999);
      console.log("服务已启动, 在http://localhost:9999")
    }
    bootstrap();
    

Swagger文档

  1. 安装

    npm i @nestjs/swagger swagger-ui-express -S
    
  2. main.ts配置

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { ValidationPipe } from '@nestjs/common';
    import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
    
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      app.useGlobalPipes(new ValidationPipe());
      // 配置 Swagger
      const options = new DocumentBuilder()
        .setTitle('film-server')
        .setDescription('电影服务器接口')
        .setVersion('1.0')
        // .addTag('test') // 添加分组标签
        .build();
      const document = SwaggerModule.createDocument(app, options);
      SwaggerModule.setup('docs', app, document); 
      await app.listen(9999);
      console.log("服务已启动, 在http://localhost:9999")
    }
    bootstrap();
    
  3. 添加参数

    import { IsString, IsNumberString, IsNotEmpty } from 'class-validator';
    import { ApiProperty } from '@nestjs/swagger';
    export class CreateUserDto {
      @IsNotEmpty({ message: 'username不能为空' })
      @IsString({ message: 'username必须是字符类型' })
      @ApiProperty()
      readonly username: string;
      @IsNumberString({ message: 'age必须是数字或者字符串' })
      @ApiProperty()
      age: number | string;
    }
    
  4. 分组标签

    import { Controller, Post, Body, UseGuards, UsePipes } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    import { AuthService } from '../auth/auth.service';
    import { UserService } from './user.service';
    import { ValidationPipe } from '../../pipe/validation.pipe';
    import { RegisterInfoDTO } from './user.dto';
    import { ApiTags } from '@nestjs/swagger';
     
    @ApiTags('user') // 添加 接口标签 装饰器
    @Controller('user')
    export class UserController {
      constructor(private readonly authService: AuthService, private readonly usersService: UserService) {}
     
      // JWT验证 - Step 1: 用户请求登录
      @Post('login')
      async login(@Body() loginParmas: any) {
        ...
      }
     
      @UseGuards(AuthGuard('jwt'))
      @UsePipes(new ValidationPipe())
      @Post('register')
      async register(@Body() body: RegisterInfoDTO) {
        return await this.usersService.register(body);
      } 
    

添加参数校验

https://blog.csdn.net/kuangshp128/article/details/97132480

一、局部验证的方式

  1. 安装包

    npm i --save class-validator class-transformer
    
  2. 比如在cat的目录下创建一个dto的文件夹,创建一个create.cat.dto.ts的文件

    import { IsString, IsInt, MinLength, MaxLength } from 'class-validator';
    export class CreateCatDto {
      @IsString({ message: '必须的字符类型' })
      @MinLength(2, {
        message: '长度不能小于2',
      })
      @MaxLength(10, {
        message: '长度不能超过10',
      })
      readonly name: string;
    
      @IsInt({ message: '必须的整数' })
      readonly age: number;
    } 
    
  3. cat.controller.ts中使用提交过来的数据约束

    @Controller('cat')
    export class CatController {
      constructor(private readonly catService: CatService) {}
      @Post()
      @HttpCode(HttpStatus.CREATED)
      async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
        Logger.log('-----------创建猫 start--------------');
        Logger.log(createCatDto);
        Logger.log('-----------创建猫 end--------------');
        return '创建猫';
      }
      ...
    } 
    

二、全局使用管道校验(前提是先安装包)

  1. 在根目录下的main.ts文件中

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { Logger, ValidationPipe } from '@nestjs/common';
    import * as helmet from 'helmet';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule, {
        cors: true, // 设置跨站访问
        logger: false,
      });
      // 使用跨站脚本攻击类的库
      app.use(helmet());
      // 给请求添加prefix
      app.setGlobalPrefix('api/v1');
      // 全局使用管道
      app.useGlobalPipes(new ValidationPipe());
      await app.listen(3000, () => {
        Logger.log('服务已经启动,请访问localhost:3000');
      });
    }
    bootstrap(); 
    
  2. cat.controller.ts中使用提交过来的数据约束

    @Controller('cat')
    export class CatController {
      constructor(private readonly catService: CatService) {}
      @Post()
      @HttpCode(HttpStatus.CREATED)
      async create(@Body() createCatDto: CreateCatDto) { // 这里就不需要使用校验约束
        Logger.log('-----------创建猫 start--------------');
        Logger.log(createCatDto);
        Logger.log('-----------创建猫 end--------------');
        return '创建猫';
      }
      ...
    } 
    

三、约束@Param接受的数据类型

四、自己创建管道生成验证规则

五、自定义返回错误格式

连接数据库